package com.yb0os1.minispring;

import com.yb0os1.minispring.anno.Component;
import com.yb0os1.minispring.design.BeanDefinition;
import com.yb0os1.minispring.design.BeanPostProcessor;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * spring是干什么的？
 * 简单的说可以 spring做两件事：造对象和拿对象
 * 怎么造对象？
 * 构造函数，但是构造函数有参数那么参数从哪里来？
 * 有属性是自动注入的那么这些属性从哪里来？
 * 生命周期的函数（如init 创建完对象之后要执行一次）由谁执行？
 */
public class ApplicationContext {

    public ApplicationContext(String packageName) throws Exception {
        initContext(packageName);
    }

    /**
     * 拿对象的函数
     */
    //根据名称获取对象
    public Object getBean(String name) {
        Object bean = ioc.get(name);
        if (bean!=null){
            return bean;
        }
        //如果等于null 判断是真的不存在还是没有加载
        if (beanDefinitionMap.containsKey(name)){
            return createBean(beanDefinitionMap.get(name));
        }
        return null;
    }

    //根据类型获取对象
    //为什么在ioc里面不直接使用类型作为key？
    // 比如我们有一个Cat类，使用Cat class作为key的话传入Cat肯定是可以获取的 但是我们如果传入Object或者接口或者父类 没有办法通过单一类型匹配拿到对应的bean
    public <T> T getBean(Class<T> beanType) {
        //isAssignableFrom beanType是否可以赋值给bean.getClass()  传进来的是bean实现的接口或者父类
       /* return ioc.values().stream()
                .filter(bean -> beanType.isAssignableFrom(bean.getClass()))
                .map(bean -> (T) bean)
                .findAny().orElse(null);*/
        //获取bean的那么就行
        String beanName = beanDefinitionMap.values().stream()
                .filter(beanDefinition -> beanType.isAssignableFrom(beanDefinition.getBeanType()))
                .map(BeanDefinition::getName)
                .findFirst()
                .orElse(null);
        return (T) getBean(beanName);
    }

    //根据类型获取对象的集合
    public <T> List<T> getBeans(Class<T> beanType) {
        return beanDefinitionMap.values().stream()
                .filter(beanDefinition -> beanType.isAssignableFrom(beanDefinition.getBeanType()))
                .map(BeanDefinition::getName)
                .map(this::getBean)
                .map(bean -> (T) bean)
                .toList();
    }

    /**
     * 造对象的函数：在上下文初始化的时候就应该造函数了
     * 流程：获取传入包名下带有特定注解的类（class）->把类变成BeanDefinition->通过设计图创建对象
     **/

    private final Map<String,Object> ioc = new HashMap<>();
    private final Map<String,BeanDefinition> beanDefinitionMap = new HashMap<>();
    //存储还没有初始化完成的对象 类似增加一层缓存来解决循环依赖
    private final Map<String,Object> loadingIoc = new HashMap<>();

    private final List<BeanPostProcessor> postProcessors = new ArrayList<>();
    public void initContext(String packageName) throws Exception {
        //问题：
        // 造什么对象？ 带有Component注解的对象
        // 怎么造对象？ 参数、属性等怎么来

        //1、拿到传入包名下的具有Component注解的类
        //2、把类变成BeanDefinition（Bean的设计图）
        //3、通过BeanDefinition创建对象
        //主流程是一个一个加载的beanDefinition  但是我们需要先加载所有的beanDefinition
//        scanPackage(packageName).stream()
//                .filter(this::isContainComponent)
//                .map(this::wrapper)
//                .forEach(this::createBean);
        scanPackage(packageName).stream()
                .filter(this::isContainComponent)
                .forEach(this::wrapper);
        initBeanPostProcessor();
        System.out.println(postProcessors);
        beanDefinitionMap.values().forEach(this::createBean);
    }

    private void initBeanPostProcessor() {
        beanDefinitionMap.values().stream()
                //获取所有BeanPostProcessor的实现类或者父类
                .filter(bd->BeanPostProcessor.class.isAssignableFrom(bd.getBeanType()))
                .map(this::createBean)
                .map(beanPostProcessor -> (BeanPostProcessor) beanPostProcessor)
                .forEach(postProcessors::add);
    }

    private Object createBean(BeanDefinition beanDefinition) {
        String name = beanDefinition.getName();
        if (ioc.containsKey(name)){
            //存在
            return ioc.get(name);
        }
        if(loadingIoc.containsKey(name)){
            return loadingIoc.get(name);
        }
        return doCreateBean(beanDefinition, name);
    }

    private Object doCreateBean(BeanDefinition beanDefinition, String name) {
        Object bean = null;
        try {
            // TODO bug？
            //  initializeBean返回的bean可能跟之前不是同一个了，导致一个问题：依赖注入的如果是从loadingIoc中取到的bean还是旧的
            Constructor<?> constructor = beanDefinition.getConstructor();
            bean = constructor.newInstance();
            loadingIoc.put(name,bean);
            autowiredFields(bean,beanDefinition);
            bean = initializeBean(bean,beanDefinition);
            loadingIoc.remove(name);
            ioc.put(name,bean);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return bean;
    }

    private Object initializeBean(Object bean, BeanDefinition beanDefinition) throws InvocationTargetException, IllegalAccessException {
        for (BeanPostProcessor postProcessor : postProcessors) {
            bean = postProcessor.beforeInitializeBean(bean, beanDefinition.getName());
        }

        Method postConstructMethod = beanDefinition.getPostConstructMethod();
        if (postConstructMethod != null){
            postConstructMethod.invoke(bean);
        }
        for (BeanPostProcessor postProcessor : postProcessors) {
            bean = postProcessor.afterInitializeBean(bean, beanDefinition.getName());
        }
        return bean;
    }

    private void autowiredFields(Object bean, BeanDefinition beanDefinition) throws IllegalAccessException {
        for (Field autowiredFiled : beanDefinition.getAutowiredFields()) {
            autowiredFiled.setAccessible(true);
            //之前写的getBean直接从ioc中返回 这样可能有点问题的  如果设计图好了但是还没有创建对象那么拿到的是null
            autowiredFiled.set(bean,getBean(autowiredFiled.getType()));
        }
    }

    //解决bean名字重复的问题
    private BeanDefinition wrapper(Class<?> type) {
        BeanDefinition beanDefinition = new BeanDefinition(type);
        if (beanDefinitionMap.containsKey(beanDefinition.getName())) {
            throw new RuntimeException("bean名字重复");
        }
        beanDefinitionMap.put(beanDefinition.getName(),beanDefinition);
        return beanDefinition;
    }

    //设置为受保护，继承的时候可以重写 这样可以实现自定义注解
    protected boolean isContainComponent(Class<?> clazz) {
        return clazz.isAnnotationPresent(Component.class);
    }

    //扫描传入的包名，获取这个包下的所有类class
    public List<Class<?>> scanPackage(String packageName) throws Exception {
        List<Class<?>> classList = new ArrayList<>();
        //getResource参数需要/或者\，这里获取是资源 找到classPath下的对应packageName的文件夹
        URL resource = this.getClass().getClassLoader().getResource(packageName.replace(".", File.separator));
        //拿到对应的文件夹 转换为path
        Path path = Paths.get(resource.toURI());
        //递归遍历这个path
        Files.walkFileTree(path, new SimpleFileVisitor<>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                Path absolutePath = file.toAbsolutePath();
                if (absolutePath.toString().endsWith(".class")) {//判断是否是class文件
                    String absolutePathString = absolutePath.toString().replace(File.separator, ".");
                    int index = absolutePathString.indexOf(packageName);
                    String className = absolutePathString.substring(index, absolutePathString.length() - ".class".length());
                    try {
                        classList.add(Class.forName(className));
                    } catch (ClassNotFoundException e) {
                        throw new RuntimeException(e);
                    }
                }
                return FileVisitResult.CONTINUE;//不管遇到什么情况都要继续递归遍历执行
            }
        });
        return classList;
    }
}
